<?php
/**
 * Zend Framework
 *
 * LICENSE
 *
 * This source file is subject to the new BSD license that is bundled
 * with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://framework.zend.com/license/new-bsd
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@zend.com so we can send you a copy immediately.
 *
 * @category   Zend
 * @package    Zend_Ldap
 * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 * @version    $Id: Ext.php 10 2008-11-08 17:17:02Z Stefan.Gehrig.HN $
 */

/** 
 * Zend_Ldap 
 */
require_once 'Zend/Ldap.php';
/** 
 * Zend_Ldap_Exception
 */
require_once 'Zend/Ldap/Exception.php';
/** 
 * Zend_Ldap_Helper 
 */
require_once 'Zend/Ldap/Helper.php';

/**
 * Zend_Ldap_Ext provides extended access to LDAP server.
 * 
 * Zend_Ldap_Ext extends Zend_Ldap with search and modify capabilities
 *
 * @category   Zend
 * @package    Zend_Ldap
 * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 */
class Zend_Ldap_Ext extends Zend_Ldap
{
	const SEARCH_SCOPE_SUB=1;
    const SEARCH_SCOPE_ONE=2;
    const SEARCH_SCOPE_BASE=3;
    
    /**
     * @param  array|Zend_Config $options Options used in connecting, binding, etc.
     */
    public function __construct($options=array())
    {
        if ($options instanceof Zend_Config) {
            $options=$options->toArray();
        }
        parent::__construct($options);
    }
    
	public function __destruct()
	{
		$this->disconnect();
	}
	
    /**
     * Gets the base DN under which objects of interest are located
     * 
     * @return string 
     */
    public function getBaseDn()
    {
        return $this->_getBaseDn();
    }
    
	/**
     * A global LDAP search routine for finding information.
     *
     * @param string $filter
     * @param string $base_dn
     * @param integer $scope
     * @param array $attributes
     * @param string $sort
     * @return Zend_Ldap_QueryResult 
     * @throws Zend_Ldap_Exception
     */ 
    public function search($filter, $basedn=null, $scope=self::SEARCH_SCOPE_SUB, 
    	array $attributes=array(), $sort=null) 
    { 
        if ($basedn===null) $basedn=$this->getBaseDn(); 
         
        if ($scope===self::SEARCH_SCOPE_ONE) 
        { 
             $search=@ldap_list($this->getResource(), $basedn, $filter, $attributes);
        } 
        else if ($scope===self::SEARCH_SCOPE_BASE) 
        { 
             $search=@ldap_read($this->getResource(), $basedn, $filter, $attributes);
        } 
        else 
        { 
        	$search=@ldap_search($this->getResource(), $basedn, $filter, $attributes);
        } 
        
        if($search===false) 
        {
            throw new Zend_Ldap_Exception($this, 'searching: ' . $filter);
        }
        if (!is_null($sort) && is_string($sort))
        {
        	$isSorted=@ldap_sort($this->getResource(), $search, $sort);
        	if($search===false) 
        	{
            	throw new Zend_Ldap_Exception($this, 'sorting: ' . $sort);
        	}
        }
        /** 
         * Zend_Ldap_QueryResult 
         */
        require_once 'Zend/Ldap/QueryResult.php';
        return new Zend_Ldap_QueryResult($this, $search); 
    } 
    
    /**
     * Count items found by given filter.
     *
     * @param string $filter
     * @param string $base_dn
     * @param integer $scope
     * @return integer 
     * @throws Zend_Ldap_Exception
     */ 
    public function count($filter, $basedn=null, $scope=self::SEARCH_SCOPE_SUB) 
    { 
        try {
            $result=$this->search($filter, $basedn, $scope, array('objectClass'), null);
        }
        catch (Zend_Ldap_Exception $e) {
            if ($e->getCode()===Zend_Ldap_Exception::LDAP_NO_SUCH_OBJECT) return 0;
            else throw $e;            
        }
        return $result->count();
    } 
    
    /**
     * Count children for a given DN.
     *
     * @param string $dn
     * @return integer 
     * @throws Zend_Ldap_Exception
     */ 
    public function countChildren($dn) 
    { 
        return $this->count('(objectClass=*)', $dn, self::SEARCH_SCOPE_ONE);
    }
    
    /**
     * Check if a given DN exists.
     *
     * @param string $dn
     * @return boolean 
     * @throws Zend_Ldap_Exception
     */ 
    public function exists($dn) 
    { 
        return ($this->count('(objectClass=*)', $dn, self::SEARCH_SCOPE_BASE)==1);
    } 
     
    /**
     * Search LDAP registry for entries matching filter and optional attributes
     *
     * @param string $filter
     * @param string $base_dn
     * @param integer $scope
     * @param array $attributes
     * @param string $sort
     * @return array 
     * @throws Zend_Ldap_Exception
     */ 
    public function searchEntries($filter, $basedn=null, $scope=self::SEARCH_SCOPE_SUB, 
    	array $attributes=array(), $sort=null) 
    { 
        $result=$this->search($filter, $basedn, $scope, $attributes, $sort);
        return $result->toArray(); 
    }
    
    /**
     * Get LDAP entry by DN
     *
     * @param string $dn
     * @param array $attributes
     * @param boolean $throwOnNotFound
     * @return array
     * @throws Zend_Ldap_Exception
     */
    public function getEntry($dn, array $attributes=array(), $throwOnNotFound=false)
    {
    	try 
    	{
    		$result=$this->search("(objectClass=*)", $dn, self::SEARCH_SCOPE_BASE, $attributes, null);
    		return $result->getFirst();
    	}
		catch (Zend_Ldap_Exception $e)
		{
			if ($throwOnNotFound) throw $e; 	
		}
    	return null;
    }
    
    /**
     * Prepares an ldap data entry array for insert/update operation
     *
     * @param array $entry
     * @return void
     * @throws InvalidArgumentException
     */
    public static function prepareLdapEntryArray(array &$entry)
    {
        if (array_key_exists('dn', $entry)) unset($entry['dn']);
        foreach ($entry as $key => $value) {
            if (is_array($value)) {
                foreach ($value as $i => $v) {
                    if (is_null($v)) unset($value[$i]);
                    else if (empty($v)) unset($value[$i]);
                    else if (!is_scalar($v)) {
                        throw new InvalidArgumentException('Only scalar values allowed in LDAP data');
                    }
                }
                $entry[$key]=array_values($value);
            } else {
               if (is_null($value)) $entry[$key]=array();
               else if (empty($value)) $entry[$key]=array();
               else $entry[$key]=array($value);
            }
        }
    }
     
    /**
     * Add new information to the LDAP repository
     * 
     * @param string $dn
     * @param array $entry
     * @return Zend_Ldap_Ext *Provides a fluid interface* 
     * @throws Zend_Ldap_Exception
     */ 
    public function add($dn, array $entry) 
    { 
    	self::prepareLdapEntryArray($entry);
    	foreach ($entry as $key => $value) {
    	    if (is_array($value)) {
    	        if (count($value)===0) unset($entry[$key]);
    	    }
    	}

    	$dnParts=Zend_Ldap_Helper::explodeDn($dn);
        $rdnParts=array_shift($dnParts);
        for ($i=0; $i<count($rdnParts); $i+=2) {
            $attr=$rdnParts[$i];
            $value=Zend_Ldap_Helper::unescapeDnValue($rdnParts[$i+1]);
            if (!in_array($value, $entry[$attr]) || count($entry[$attr])!==1) {
                $entry[$attr]=array($value);
            }
        }
    	
        $isAdded=@ldap_add($this->getResource(), $dn, $entry); 
        if($isAdded===false) 
        { 
            throw new Zend_Ldap_Exception($this, 'adding: ' . $dn); 
        } 
        return $this; 
    } 
     
    /**
     * Update LDAP registry
     * 
     * @param string $dn
     * @param array $entry
     * @return Zend_Ldap_Ext *Provides a fluid interface*
     * @throws Zend_Ldap_Exception
     */ 
    public function update($dn, array $entry) 
    { 
    	self::prepareLdapEntryArray($entry);
        $isModified=@ldap_modify($this->getResource(), $dn, $entry); 
        if($isModified===false) 
        { 
            throw new Zend_Ldap_Exception($this, 'updating: ' . $dn); 
        } 
        return $this; 
    } 
    
    /**
     * Save entry to LDAP registry.
     * 
     * Internally decides if entry will be updated to added by calling
     * {@link exists()}.
     * 
     * @param string $dn
     * @param array $entry
     * @return Zend_Ldap_Ext *Provides a fluid interface*
     * @throws Zend_Ldap_Exception
     */ 
    public function save($dn, array $entry) 
    { 
        if ($this->exists($dn)) $this->update($dn, $entry);
        else $this->add($dn, $entry);
        return $this; 
    } 
     
    /**
     * Delete an LDAP entry
     *
     * @param  string $dn
     * @param  boolean $recursively
     * @return Zend_Ldap_Ext *Provides a fluid interface*
     * @throws Zend_Ldap_Exception
     */ 
    public function delete($dn, $recursively=false) 
    { 
        if ($recursively===true) {
            if ($this->countChildren($dn)>0) {
                $children=array();
                $search=@ldap_list($this->getResource(), $dn, '(objectClass=*)', array('objectClass'));
                for ($entry=@ldap_first_entry($this->getResource(), $search);
                        $entry!==false;
                        $entry=@ldap_next_entry($this->getResource(), $entry))
                {
                    $childDn=@ldap_get_dn($this->getResource(), $entry);
                    if ($childDn===false) {
                        throw new Zend_Ldap_Exception($this, 'getting dn');
                    }
                    $children[]=$childDn;
                }
                @ldap_free_result($search);
                foreach ($children as $c) {
                    $this->delete($c, true);   
                }
            }
        }
        $isDeleted=@ldap_delete($this->getResource(), $dn); 
        if($isDeleted===false) { 
            throw new Zend_Ldap_Exception($this, 'deleting: ' . $dn); 
        } 
        return $this; 
    }
    
    /**
     * Moves a LDAP entry from one DN to another subtree.
     *
     * @param string $from
     * @param string $to
     * @param boolean $recursively
     * @param boolean $alwaysEmulate
     * @return Zend_Ldap_Ext *Provides a fluid interface*
     * @throws Zend_Ldap_Exception
     */
    public function moveToSubtree($from, $to, $recursively=false, $alwaysEmulate=false)
    {
        $orgDnParts=Zend_Ldap_Helper::explodeDn($from);
        $newParentDnParts=Zend_Ldap_Helper::explodeDn($to);
        $newDnParts=array_merge(array(array_shift($orgDnParts)), $newParentDnParts);
        $newDn=Zend_Ldap_Helper::implodeDn($newDnParts);  
        return $this->rename($from, $newDn, $recursively, $alwaysEmulate);
    }
    
    /**
     * Moves a LDAP entry from one DN to another DN.
     * 
     * This is an alias for {@link rename()}
     *
     * @param string $from
     * @param string $to
     * @param boolean $recursively
     * @param boolean $alwaysEmulate
     * @return Zend_Ldap_Ext *Provides a fluid interface*
     * @throws Zend_Ldap_Exception
     */
    public function move($from, $to, $recursively=false, $alwaysEmulate=false)
    {
        return $this->rename($from, $to, $recursively, $alwaysEmulate);
    }
    
    /**
     * Renames a LDAP entry from one DN to another DN.
     * 
     * This method implicitely moves the entry to another location within the tree.
     *
     * @param string $from
     * @param string $to
     * @param boolean $recursively
     * @param boolean $alwaysEmulate
     * @return Zend_Ldap_Ext *Provides a fluid interface*
     * @throws Zend_Ldap_Exception
     */
    public function rename($from, $to, $recursively=false, $alwaysEmulate=false)
    {
        $emulate=(bool)$alwaysEmulate;
        if (!function_exists('ldap_rename')) $emulate=true;
        else if ($recursively) $emulate=true;
        
        if ($emulate===false) {
            $newDnParts=Zend_Ldap_Helper::explodeDn($to);
            $newRdn=Zend_Ldap_Helper::implodeRdn(array_shift($newDnParts));
            $newParent=Zend_Ldap_Helper::implodeDn($newDnParts);
            $isOK=@ldap_rename($this->getResource(), $from, $newRdn, $newParent, true);
            if($isOK===false) { 
                throw new Zend_Ldap_Exception($this, 'renaming ' . $from . ' to ' . $to); 
            }
            else if (!$this->exists($to)) $emulate=true;
        }
        if ($emulate) {
            $this->copy($from, $to, $recursively);            
            $this->delete($from, $recursively);
        }
        return $this;
    }
    
    /**
     * Copies a LDAP entry from one DN to another subtree.
     *
     * @param string $from
     * @param string $to
     * @param boolean $recursively
     * @return Zend_Ldap_Ext *Provides a fluid interface*
     * @throws Zend_Ldap_Exception
     */
    public function copyToSubtree($from, $to, $recursively=false)
    {
        $orgDnParts=Zend_Ldap_Helper::explodeDn($from);
        $newParentDnParts=Zend_Ldap_Helper::explodeDn($to);
        $newDnParts=array_merge(array(array_shift($orgDnParts)), $newParentDnParts);
        $newDn=Zend_Ldap_Helper::implodeDn($newDnParts);
        return $this->copy($from, $newDn, $recursively);
    }
    
    /**
     * Copies a LDAP entry from one DN to another DN.
     *
     * @param string $from
     * @param string $to
     * @param boolean $recursively
     * @return Zend_Ldap_Ext *Provides a fluid interface*
     * @throws Zend_Ldap_Exception
     */
    public function copy($from, $to, $recursively=false)
    {
        $entry=$this->getEntry($from, array(), true);
        
        $toDnParts=Zend_Ldap_Helper::explodeDn($to);
        $this->add($to, $entry);
        
        if ($recursively===true && $this->countChildren($from)>0) {
            $children=array();
            $search=@ldap_list($this->getResource(), $from, '(objectClass=*)', array('objectClass'));
            for ($e=@ldap_first_entry($this->getResource(), $search);
                    $e!==false;
                    $e=@ldap_next_entry($this->getResource(), $e))
            {
                $cdn=@ldap_get_dn($this->getResource(), $e);
                if ($cdn===false) {
                    throw new Zend_Ldap_Exception($this, 'getting dn');
                }
                $children[]=$cdn;
            }
            @ldap_free_result($search);
            foreach ($children as $c) {
                $cDnParts=Zend_Ldap_Helper::explodeDn($c);
                $newChildParts=array_merge(array(array_shift($cDnParts)), $toDnParts);
                $newChild=Zend_Ldap_Helper::implodeDn($newChildParts);  
                $this->copy($c, $newChild, true);
            }        
        }
        return $this;
    }
    
    /**
     * Returns the specified DN as a Zend_Ldap_Node
     *
     * @param  string $dn
     * @return Zend_Ldap_Node
     * @throws Zend_Ldap_Exception
     */
    public function getNode($dn)
    {
        /** 
         * Zend_Ldap_Node 
         */
        require_once 'Zend/Ldap/Node.php';
        return Zend_Ldap_Node::fromLdap($dn, $this);
    }
    
    /**
     * Returns the base node as a Zend_Ldap_Node
     *
     * @return Zend_Ldap_Node
     * @throws Zend_Ldap_Exception
     */
    public function getBaseNode()
    {
        return $this->getNode($this->getBaseDn(), $this);
    }
}